package ua.yandex.shad.fatenkotemperature;

import java.util.ArrayList;
import Jama.Matrix;
import Jama.SingularValueDecomposition;

/**
 *
 * @author vlad
 */
public class TemperatureSeriesAnalysis {

    private static final double eps = 1E-10;

    private static final double MIN_TEMP = -273.0;

    private final double[] temperatureSeries;
    
    private boolean checkLength(int size){
        if(this.temperatureSeries == null){
            //unreachable branch at normal conditions
            throw new NullPointerException();
        }
        if(this.temperatureSeries.length > size){
            return true;
        }
        return false;
    }

    public TemperatureSeriesAnalysis(double[] temperatureSeries) throws
            IllegalTemperatureValueException {
        if (temperatureSeries == null) {
            throw new NullPointerException();
        }
        this.temperatureSeries = new double[temperatureSeries.length];
        for (int i = 0; i < this.temperatureSeries.length; i++) {
            if (temperatureSeries[i] < MIN_TEMP) {
                throw new IllegalTemperatureValueException();
            }
            this.temperatureSeries[i] = temperatureSeries[i];
        }

    }

    public double calcAverageTemp() {
        if (!checkLength(0)) {
            return 0;
        }
        double sum = 0;
        for (int i = 0; i < this.temperatureSeries.length; i++) {
            sum += temperatureSeries[i];
        }
        sum /= temperatureSeries.length;
        return sum;
    }

    public double calcTempDeviation() {
        if (!checkLength(0)) {
            return 0;
        }
        double deviation = 0;
        for (int i = 0; i < this.temperatureSeries.length; i++) {
            deviation += this.temperatureSeries[i] * this.temperatureSeries[i];
        }

        deviation -= Math.pow(calcAverageTemp(), 2);
        return deviation;
    }

    public double findTempClosestToZero() {
        return findTempClosestToValue(0);
    }

    public double findTempClosestToValue(double value) {
        if (!checkLength(0)) {
            return 0;
        }
        double closest = this.temperatureSeries[0] - value;
        for (int i = 1; i < this.temperatureSeries.length; i++) {
            if (Math.abs(this.temperatureSeries[i] - value) <= Math.abs(closest)) {
                if (Math.abs(this.temperatureSeries[i] - value) != Math.abs(closest)) {
                    closest = this.temperatureSeries[i] - value;
                } else {
                    if (closest != this.temperatureSeries[i] && closest < 0) {
                        closest = -closest;
                    }
                }
            }
        }
        return closest + value;
    }

    public double[] findLessThen(double tempValue) {
        ArrayList<Double> subset;
        subset = new ArrayList<>();
        for (int i = 0; i < this.temperatureSeries.length; i++) {
            if (this.temperatureSeries[i] < tempValue) {
                subset.add(this.temperatureSeries[i]);
            }
        }
        //dunno how to avoid this crutch
        double[] unboxer;
        unboxer = new double[subset.size()];
        for (int i = 0; i < subset.size(); i++) {
            unboxer[i] = subset.get(i);
        }
        return unboxer;
    }

    public double[] findGreaterThen(double tempValue) {
        ArrayList<Double> subset;
        subset = new ArrayList<>();
        for (int i = 0; i < this.temperatureSeries.length; i++) {
            if (this.temperatureSeries[i] >= tempValue) {
                subset.add(this.temperatureSeries[i]);
            }
        }
        //dunno how to avoid this crutch
        double[] unboxer;
        unboxer = new double[subset.size()];
        for (int i = 0; i < subset.size(); i++) {
            unboxer[i] = subset.get(i);
        }
        return unboxer;
    }

    private double forecastNext(double[][] pred, Matrix V, Matrix w) {
        Matrix Q = new Matrix(pred);
        Matrix res = w.times((V.transpose().times(V)).inverse())
                .times(V.transpose()).times(Q);
        return res.get(0, 0);
    }

    public double[] forecastTemp() throws NotEnoughValuesToForecastException {
        if (!checkLength(9)) {
            throw new NotEnoughValuesToForecastException();
        }
        int winLen;
        int winTime;

        //preparing trajectory matrix
        winLen = this.temperatureSeries.length / 2;
        winTime = this.temperatureSeries.length + 1 - winLen;
        double[][] trajectArray = new double[winLen][winTime];
        for (int i = 0; i < winLen; i++) {
            for (int j = 0; j < winTime; j++) {
                trajectArray[i][j] = this.temperatureSeries[i + j];
            }
        }

        Matrix trajectMatrix = new Matrix(trajectArray);
        //trajectory  matrix filled

        //creating covariation matrix
        Matrix covarMatrix = (trajectMatrix.times(trajectMatrix.transpose()))
                .times(1. / winTime);
        //covariation matrix created

        //represent covariation matrix using SVD
        SingularValueDecomposition decomposed = covarMatrix.svd();
        //SVD interpretation formed

        //forming main group
        double[] singularVals = decomposed.getSingularValues();
        int size = 0;
        for (int i = 0; i < singularVals.length; i++) {
            if (Math.abs(singularVals[i]) > eps) {
                size++;
            }
        }
        int[] mainGroup = new int[size];
        size = 0;
        for (int i = 0; i < singularVals.length; i++) {
            if (Math.abs(singularVals[i]) > eps) {
                mainGroup[size] = i;
                size++;
            }
        }
        //formed main group

        //preparing extracted data to forecasting
        Matrix directMatrix = decomposed.getU().getMatrix(0, winLen - 1, mainGroup);
        Matrix upperD = directMatrix.getMatrix(0, winLen - 2, 0, size - 1);
        Matrix lowerD = directMatrix.getMatrix(winLen - 1, winLen - 1, 0, size - 1);
        //data prepared

        //forecasting
        double[] result = new double[3];
        double[][] pred = new double[winLen - 1][1];
        for (int i = winTime; i < this.temperatureSeries.length; i++) {
            pred[i - winTime][0] = this.temperatureSeries[i];
        }
        result[0] = forecastNext(pred, upperD, lowerD);
        if (result[0] < MIN_TEMP) {
            result[0] = MIN_TEMP;
        }
        for (int i = 1; i < pred.length; i++) {
            pred[i - 1][0] = pred[i][0];
        }
        pred[winLen - 2][0] = result[0];
        result[1] = forecastNext(pred, upperD, lowerD);
        if (result[1] < MIN_TEMP) {
            result[1] = MIN_TEMP;
        }
        for (int i = 1; i < pred.length; i++) {
            pred[i - 1][0] = pred[i][0];
        }
        pred[winLen - 2][0] = result[1];
        result[2] = forecastNext(pred, upperD, lowerD);
        if (result[2] < MIN_TEMP) {
            result[2] = MIN_TEMP;
        }
        //forecast finished

        return result;
    }

}
